Для данного модуля необходимы внешние библиотеки Загружаем пакеты: RPostgres, plotly, ggplot2
Изначально была идея использовать для данных общую бд, однако в ходе разработки было решено, что использование csv файлов практичнее и быстрее.
Получим данные обо всех игровых моментах из таблицы plays
plays_df <- as.data.frame(read.csv('nfl-big-data-bowl-2021/plays.csv'))
head(plays_df)
Получим координаты всех игроков в игровые моменты первой недели, используя week1.csv
df_week1 <- as.data.frame(read.csv('nfl-big-data-bowl-2021/week1.csv'))
head(df_week1)
Для отрисовки игрового момента используем функцию библиотеки ggplot2. Для этого мы берем gameId и playId - определяющие ключи для каждого игрового момента. На основе них получаем координаты во всех кадрах для игроков. Отсортировав покадровую расстановку игроков мы можем выполнить отрисовку каждой точки.
plot_play_moment <- function(gameId, playId) {
slice_df <- df_week[df_week$gameId==gameId,]
slice_df <- slice_df[slice_df$playId == playId,]
slice_df <-slice_df[order(slice_df$frameId, slice_df$nflId),]
play_moment <- plays_df[plays_df$gameId==gameId & plays_df$playId == playId,]
print(play_moment$playDescription)
plt <- ggplot(slice_df, aes(frame = frameId)) +
annotate("rect", xmin = 0, xmax = 10, ymin = 0, ymax = 53,fill='red', alpha=.2) +
annotate("rect", xmin = 110, xmax = 120, ymin = 0, ymax = 53,fill='red', alpha=.2) +
annotate("rect", xmin = 10, xmax = 110, ymin = 0, ymax = 53,fill='green', alpha=.2) +
xlim(0, 120) + ylim(-10, 60) +
geom_vline(xintercept = seq(0, 120, 10)) +
geom_vline(xintercept = play_moment$absoluteYardlineNumber, colour='red') +
geom_hline(yintercept = seq(0, 53, 53)) +
geom_point(mapping=aes(x, y,colour=team), size=3) +
geom_text(mapping=aes(x, y, label = position), size=2.5) +
labs(x = "X",
y = "Y",
colour="Team") +
scale_color_manual(values=c("#6495ED", "#32CD32", "#F08080"))
ggplotly(plt) %>%
layout(title = list(text = paste0(paste("NFL 2018 - ", gameId, play_moment$quarter, '-', play_moment$down),
'<br>',
'<sup>',
play_moment$playDescription,
'</sup>')), margin=margin(b=1))
}
Просматривая моменты, захотелось отыскать эти кадры в реальных трансляциях. Вот некотрые интересные моменты сопоставленные с видео на серверах YouTube
gameId=2018090600, playId=2474 - https://youtu.be/0fLqHxm90mM?t=286
gameId=2018092400, playId=539 - https://youtu.be/U046WMQ9Ie8?t=80
gameId=2018090600, playId=344 - https://youtu.be/0fLqHxm90mM?t=74
df_week <- df_week1
plot_play_moment(2018090600, 344)
## [1] "(9:24) (Shotgun) N.Foles pass incomplete short left to D.Sproles (R.Allen)."
2018090600,2474 - https://youtu.be/0fLqHxm90mM?t=286
df_week <- df_week1
plot_play_moment(2018090600,2474)
## [1] "(11:15) M.Ryan pass incomplete deep right to J.Jones (J.Mills) [D.Barnett]. Atlanta challenged the incomplete pass ruling, and the play was Upheld. The ruling on the field stands. (Timeout #1.)"
Такая отрисовка во многом проще для восприятия и позволяет нам лучше понять игровой момент. А покадровое перемещение игроков может прояснить некоторые спорные моменты. Такое представление удобно будет использовать для разрешения спорных ситуаций.
Ранее в отчете были получены наилучшие позиции игроков защиты. Из данных были отобраны реальные игровые моменты, расположение защиты в которых совпадает с ними. В качестве приикладного использования функции plot_play_moment приведем анимацию такого момента. Так как этот момент произошел на 7 неделе, загрузим week7.csv в переменную df_week.
Этот момент “вживую” - https://youtu.be/U4SzvAfjR0o?t=2191
df_week7 <- as.data.frame(read.csv('nfl-big-data-bowl-2021/week7.csv'))
df_week <- df_week7
plot_play_moment(2018102106,1177)
## [1] "(7:42) (Shotgun) B.Osweiler pass incomplete short left to K.Drake."
Мы видим, что даже в самом напряженном случае (расстояние до тачдауна 5 ярдов) защита успешно работает
BMI - это показатель массы человека (на русском звучит, как индекс массы тела). Это весьма простой параметр который неплохо может оценить весовое соотношение человека.
Для начала получим данные об игроках. Мы будем использовать исправленные данные об игроках. В исходной таблице игроков рост и вес был записан в разных форматах, для упрощения работы, был использован скрипт на языке Python. Так же загрузим таблицу с рассчитанными кластерами, она пригодится нам позже.
players_df <- as.data.frame(read.csv('nfl-big-data-bowl-2021/fix.fix.players.csv'))
clustered <- as.data.frame(read.csv('clustering.csv'))
head(players_df)
В среднем BMI можно разделить на несколько категорий:
Недобор = < 18.5 Нормальный вес = 18.5–24.9 Сверх нормы = 25–29.9 Ожирение = 30 или больше
Однако мы не можем говорить о мышечной массе спортсменов, нам недостаточно информации. Поэтому мы будем рассматривать BMI, как весовой показатель. Ведь чем выше масса тем сильнее удар на скорости, в независимости масса жира или мышц.
Чтобы подсчитать индекс массы тела используем формулу для фунтов:
\(BMI = (weight / height^2) * 703\)
players_df$BMI <- (players_df$weight / (players_df$height)^2) * 703
Разобьем на три категории команд:
Расшифровка игроков
Защита ‘CB’, : CornerBack ‘SS’, : Strong Safety ‘MLB’, : Middle LineBacker ‘OLB’, : Outer LineBacker ‘LB’, : LineBacker ‘ILB’, : Inside LineBacker ‘DT’ : Defense Tackle ‘S’, : Safety ‘DE’, : Defensive End ‘DB’, : Defensive Back ‘FS’, : Free Safety
Атака ‘RB’, : Running Back ‘QB’, : Quater Back ‘WR’, : Wide Receiver ‘FB’, : FullBack ‘TE’, : Tight End ‘HB’, : HalfBack ‘NT’, : Nose Tackle
Специальная команда ‘P’, : Punter ‘LS’, : LongSnaper ‘K’, : Kicker
offence_df <- subset(players_df, (players_df$position %in% c('QB','WR','RB','FB', 'TE', 'NT', 'HB')))
defence_df <- subset(players_df, !(players_df$position %in% c('K', 'P', 'LS', 'QB','WR','RB','FB', 'TE', 'NT', 'HB')))
Мы видим, что основная часть команды защиты находится около группы с BMI выше нормы. Самые крупные игроки это DE и DT, далее идут игроки из категории LineBacker - они находятся на границе 30 BMI. И наконец игроки Defensive Back находятся в некоторых пределах нормы 25-30 BMI
plt <- ggplot(data=defence_df, aes(as.factor(position), BMI)) +
geom_point(aes(color=factor(position)), position='jitter', alpha=0.2) +
geom_boxplot(aes(group=factor(position)), alpha = 0.1, outlier.size = -Inf) +
geom_hline(yintercept = seq(25,30,5), colour='red')
plot(plt + labs(title = 'BMI by position (Defence)', x = 'Position', y = 'BMI', color = 'Position'))
Для защитников оценим их разделение по трем большим категориям: Defensive Line, Defensive Backs и Linebackers: Для этого вернемся к описанию ролей представленному нами в начале отчета:
Defensive line (DL) включает в себя:
– Defensive tackle (DT)
– Defensive end (DE)
Linebackers (LB):
– Middle linebacker (MLB)
– Outside linebacker (OLB)
Defensive backs (DB):
– Cornerback (CB)
– Safety (S)
Всего в наших данных представленны такие роли защитников: CB DB DE DT FS ILB LB MLB OLB S SS
Таким образом получаем такие категории:
Defensive line: DT DE
Linebackers: ILB LB MLB OLB
Defensive backs: CB DB S SS FS
Используя полученные категории разделим защитников на категории:
defence_df['category'] <- ifelse(defence_df$position %in% c('DT', 'DE'),
'DL',
ifelse(defence_df$position %in% c('ILB', 'LB', 'MLB', 'OLB'),
'LB',
'DB')
)
Добавим посчитанные при кластеризации очки игрокам и повторим построение графика зависимости points и BMI, используя полученные категории игроков. Для получения информации о самых эффектифных игроках посмотрим на зависимость \(points/max(points)/cnt\). Таким образом мы пронормируем очки от -1 до 1, а затем уменьшим их обратно пропорционально количеству игр. Таким образом интересующий нас кластер окажется около 0. То есть, чем чаще игрок встречался в игре, тем меньше он должен получить оченку. Иначе мы не сможем отличить игрока, пропустил 99 очков за 3 матча или пропускал по 3 очка 33 игры.
points_def_df <- merge(clustered, defence_df)
points_def_df['weighted_pnts'] = points_def_df$points /
max(points_def_df$points) /
points_def_df$cnt
plt <- ggplot(data=points_def_df, aes(BMI, weighted_pnts)) +
geom_point(aes(color=factor(points_bmi), alpha=0.7)) +
geom_hline(yintercept = 0, colour='red')
ggplotly(plt + labs(title = 'Weighted Points by BMI (Defence)', x = 'BMI', y = 'Weighted Points', color = 'Cluster'))
Обратим внимание на 5 кластер - он достаточно растянут по BMI и при этом находится около 0, причем с обеих сторон, то есть эти игроки приносили даже отрицательные очки, что хорошо для защиты.
Повторим вывод 5 кластера, но с разделением по категориям игроков.
best_def_df <- points_def_df[points_def_df$points_cnt == 5,]
plt <- ggplot(data=best_def_df, aes(BMI, weighted_pnts)) +
geom_point(aes(color=factor(category), alpha=0.7)) +
geom_hline(yintercept = 0, colour='red')
ggplotly(plt + labs(title = 'Weighted Points by BMI (Defence)', x = 'BMI', y = 'Weighted Points', color = 'Category'))
Мы получили достаточно четкое распределение по ролям, давайте выведем топ лучших в каждой категории.
db_best <- best_def_df[best_def_df$category == 'DB',]
top_10_db <-db_best[order(-db_best$cnt, db_best$points),]
top_10_db %>% select(1, 6, 7, 8, 9, 10, 13, 14)
lb_best <- best_def_df[best_def_df$category == 'LB',]
top_10_lb <-lb_best[order(-lb_best$cnt, lb_best$points),]
top_10_lb %>% select(1, 6, 7, 8, 9, 10, 13, 14)
dl_best <- best_def_df[best_def_df$category == 'DL',]
top_10_dl <-dl_best[order(-dl_best$cnt, dl_best$points),]
top_10_dl %>% select(1, 6, 7, 8, 9, 10, 13, 14)
Мы видим, что данные соответствуют разделению по кластеризации, сделанному в первой части отчета. Самые эффективные защитники - DL. Они приносят больше всего очков команде защиты. Однако, как мы видим по данным, их в разы меньше игроков DB и LB. Это так же влияет на распределение.
Проверим теперь распределение для игроков атаки, как их веса соотносятся с друг другом? Могут ли быть слабые стороны в подборе противников тренерами?
Среди атакующих выделяются игроки NT. Именно они “летят” на тэклов защиты. Остальные находятся на границе в 30 BMI. Самыми легкими оказались WR - они должны быть быстрыми и ловкими, чтобы суметь поймать мяч и пройти дальше за линию защиты.
plt2 <- ggplot(data=offence_df, aes(as.factor(position), BMI)) +
geom_point(aes(color=factor(position)), position='jitter', alpha=0.2) +
geom_boxplot(aes(group=factor(position)), alpha = 0.3, outlier.size = -Inf) +
geom_hline(yintercept = seq(25,30,5), colour='red')
plot(plt2 + labs(title = 'BMI by position (Offence)', x = 'Position', y = 'BMI', color = 'Position'))
Таким образом получается что каждая категория игроков имеет схожие биологические параметры. А что более значимо, игроки которые стоят против друг друга (например, DT и NT) так же имеют сходные параметры. Это означает, что тренеры команд специально подбирают игроков в соответствии с формацией противника.